#include "ibm.h"
#include "io.h"
#include "mouse.h"
#include "pic.h"
#include "serial.h"
#include "timer.h"

enum { SERIAL_INT_LSR = 1, SERIAL_INT_RECEIVE = 2, SERIAL_INT_TRANSMIT = 4, SERIAL_INT_MSR = 8 };

SERIAL serial1, serial2;

void serial_reset() {
        serial1.iir = serial1.ier = serial1.lcr = 0;
        serial2.iir = serial2.ier = serial2.lcr = 0;
        serial1.fifo_read = serial1.fifo_write = 0;
        serial2.fifo_read = serial2.fifo_write = 0;
}

void serial_update_ints(SERIAL *serial) {
        int stat = 0;

        serial->iir = 1;

        if ((serial->ier & 4) && (serial->int_status & SERIAL_INT_LSR)) /*Line status interrupt*/
        {
                stat = 1;
                serial->iir = 6;
        } else if ((serial->ier & 1) && (serial->int_status & SERIAL_INT_RECEIVE)) /*Received data available*/
        {
                stat = 1;
                serial->iir = 4;
        } else if ((serial->ier & 2) && (serial->int_status & SERIAL_INT_TRANSMIT)) /*Transmit data empty*/
        {
                stat = 1;
                serial->iir = 2;
        } else if ((serial->ier & 8) && (serial->int_status & SERIAL_INT_MSR)) /*Modem status interrupt*/
        {
                stat = 1;
                serial->iir = 0;
        }

        if (stat && ((serial->mctrl & 8) || PCJR))
                picintlevel(1 << serial->irq);
        else
                picintc(1 << serial->irq);
}

void serial_write_fifo(SERIAL *serial, uint8_t dat) {
        //        pclog("serial_write_fifo %02X\n", serial->lsr);
        serial->fifo[serial->fifo_write] = dat;
        serial->fifo_write = (serial->fifo_write + 1) & 0xFF;
        if (!(serial->lsr & 1)) {
                serial->lsr |= 1;
                serial->int_status |= SERIAL_INT_RECEIVE;
                serial_update_ints(serial);
        }
}

uint8_t serial_read_fifo(SERIAL *serial) {
        if (serial->fifo_read != serial->fifo_write) {
                serial->dat = serial->fifo[serial->fifo_read];
                serial->fifo_read = (serial->fifo_read + 1) & 0xFF;
        }
        return serial->dat;
}

void serial_write(uint16_t addr, uint8_t val, void *p) {
        SERIAL *serial = (SERIAL *)p;
        //        pclog("Write serial %03X %02X %04X:%04X\n",addr,val,CS,pc);
        switch (addr & 7) {
        case 0:
                if (serial->lcr & 0x80) {
                        serial->dlab1 = val;
                        return;
                }
                serial->thr = val;
                serial->lsr |= 0x20;
                serial->int_status |= SERIAL_INT_TRANSMIT;
                serial_update_ints(serial);
                if (serial->mctrl & 0x10) {
                        serial_write_fifo(serial, val);
                }
                break;
        case 1:
                if (serial->lcr & 0x80) {
                        serial->dlab2 = val;
                        return;
                }
                serial->ier = val & 0xf;
                serial_update_ints(serial);
                break;
        case 2:
                if (serial->has_fifo)
                        serial->fcr = val;
                break;
        case 3:
                serial->lcr = val;
                break;
        case 4:
                if ((val & 2) && !(serial->mctrl & 2)) {
                        if (serial->rcr_callback)
                                serial->rcr_callback((struct SERIAL *)serial, serial->rcr_callback_p);
                        //                        pclog("RCR raised! sending M\n");
                }
                serial->mctrl = val;
                if (val & 0x10) {
                        uint8_t new_msr;

                        new_msr = (val & 0x0c) << 4;
                        new_msr |= (val & 0x02) ? 0x10 : 0;
                        new_msr |= (val & 0x01) ? 0x20 : 0;

                        if ((serial->msr ^ new_msr) & 0x10)
                                new_msr |= 0x01;
                        if ((serial->msr ^ new_msr) & 0x20)
                                new_msr |= 0x02;
                        if ((serial->msr ^ new_msr) & 0x80)
                                new_msr |= 0x08;
                        if ((serial->msr & 0x40) && !(new_msr & 0x40))
                                new_msr |= 0x04;

                        serial->msr = new_msr;
                }
                break;
        case 5:
                serial->lsr = val;
                if (serial->lsr & 0x01)
                        serial->int_status |= SERIAL_INT_RECEIVE;
                if (serial->lsr & 0x1e)
                        serial->int_status |= SERIAL_INT_LSR;
                if (serial->lsr & 0x20)
                        serial->int_status |= SERIAL_INT_TRANSMIT;
                serial_update_ints(serial);
                break;
        case 6:
                serial->msr = val;
                if (serial->msr & 0x0f)
                        serial->int_status |= SERIAL_INT_MSR;
                serial_update_ints(serial);
                break;
        case 7:
                serial->scratch = val;
                break;
        }
}

uint8_t serial_read(uint16_t addr, void *p) {
        SERIAL *serial = (SERIAL *)p;
        uint8_t temp = 0;
        //        pclog("Read serial %03X %04X(%08X):%04X %i %i  ", addr, CS, cs, pc, mousedelay, ins);
        switch (addr & 7) {
        case 0:
                if (serial->lcr & 0x80) {
                        temp = serial->dlab1;
                        break;
                }

                serial->lsr &= ~1;
                serial->int_status &= ~SERIAL_INT_RECEIVE;
                serial_update_ints(serial);
                temp = serial_read_fifo(serial);
                if (serial->fifo_read != serial->fifo_write)
                        timer_set_delay_u64(&serial->receive_timer, 1000 * TIMER_USEC);
                break;
        case 1:
                if (serial->lcr & 0x80)
                        temp = serial->dlab2;
                else
                        temp = serial->ier;
                break;
        case 2:
                temp = serial->iir;
                if ((temp & 0xe) == 2) {
                        serial->int_status &= ~SERIAL_INT_TRANSMIT;
                        serial_update_ints(serial);
                }
                if (serial->fcr & 1)
                        temp |= 0xc0;
                break;
        case 3:
                temp = serial->lcr;
                break;
        case 4:
                temp = serial->mctrl;
                break;
        case 5:
                if (serial->lsr & 0x20)
                        serial->lsr |= 0x40;
                serial->lsr |= 0x20;
                temp = serial->lsr;
                if (serial->lsr & 0x1f)
                        serial->lsr &= ~0x1e;
                //                serial.lsr |= 0x60;
                serial->int_status &= ~SERIAL_INT_LSR;
                serial_update_ints(serial);
                break;
        case 6:
                temp = serial->msr;
                serial->msr &= ~0x0f;
                serial->int_status &= ~SERIAL_INT_MSR;
                serial_update_ints(serial);
                break;
        case 7:
                temp = serial->scratch;
                break;
        }
        //        pclog("%02X\n",temp);
        return temp;
}

void serial_receive_callback(void *p) {
        SERIAL *serial = (SERIAL *)p;

        if (serial->fifo_read != serial->fifo_write) {
                serial->lsr |= 1;
                serial->int_status |= SERIAL_INT_RECEIVE;
                serial_update_ints(serial);
        }
}

/*Tandy might need COM1 at 2f8*/
void serial1_init(uint16_t addr, int irq, int has_fifo) {
        memset(&serial1, 0, sizeof(serial1));
        io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial1);
        serial1.irq = irq;
        serial1.addr = addr;
        serial1.rcr_callback = NULL;
        timer_add(&serial1.receive_timer, serial_receive_callback, &serial1, 0);
        serial1.has_fifo = has_fifo;
}
void serial1_set(uint16_t addr, int irq) {
        serial1_remove();
        io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial1);
        serial1.irq = irq;
        serial1.addr = addr;
}
void serial1_set_has_fifo(int has_fifo) { serial1.has_fifo = has_fifo; }
void serial1_remove() { io_removehandler(serial1.addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial1); }

void serial2_init(uint16_t addr, int irq, int has_fifo) {
        memset(&serial2, 0, sizeof(serial2));
        io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial2);
        serial2.irq = irq;
        serial2.addr = addr;
        serial2.rcr_callback = NULL;
        timer_add(&serial2.receive_timer, serial_receive_callback, &serial2, 0);
        serial2.has_fifo = has_fifo;
}
void serial2_set(uint16_t addr, int irq) {
        serial2_remove();
        io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial2);
        serial2.irq = irq;
        serial2.addr = addr;
}
void serial2_set_has_fifo(int has_fifo) { serial2.has_fifo = has_fifo; }
void serial2_remove() { io_removehandler(serial2.addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial2); }
